Skip to main content

Strategy pattern

Strategy Pattern in JS

The Strategy Pattern is one of the cleanest ways to remove if/else or switch hell in Node.js.

Think of it like this:

"I have multiple ways to do the same task, and I want to swap them dynamically."


What is Strategy Pattern?

It is a behavioral design pattern where:

  • you define a common contract (a shape all strategies must follow)
  • create multiple implementations (strategies)
  • choose one strategy at runtime

Real-world analogy

Imagine a payment system:

  • Credit Card payment
  • UPI payment
  • PayPal payment

All of them do the same thing:

pay(amount)

But the implementation differs.

Instead of doing this:

if (paymentType === 'upi') {
// UPI logic
} else if (paymentType === 'card') {
// Card logic
} else if (paymentType === 'paypal') {
// PayPal logic
}

You use Strategy Pattern.


Structure

It has 3 parts:

1. Strategy Contract

Defines the method every strategy must implement (enforced by convention or JSDoc).

2. Concrete Strategies

Actual implementations.

3. Context

The class that uses the strategy.


Class-based Example

Problem: Different discount calculation strategies

Strategy contract (via JSDoc)

/**
* @typedef {Object} DiscountStrategy
* @property {function(number): number} calculate
*/

Concrete strategies

class NoDiscountStrategy {
calculate(price) {
return price;
}
}

class FlatDiscountStrategy {
constructor(discountAmount) {
this.discountAmount = discountAmount;
}

calculate(price) {
return price - this.discountAmount;
}
}

class PercentageDiscountStrategy {
constructor(percentage) {
this.percentage = percentage;
}

calculate(price) {
return price - (price * this.percentage) / 100;
}
}

Context class

class PriceCalculator {
constructor(strategy) {
this.strategy = strategy;
}

setStrategy(strategy) {
this.strategy = strategy;
}

getFinalPrice(price) {
return this.strategy.calculate(price);
}
}

Usage

const calculator = new PriceCalculator(new NoDiscountStrategy());

console.log(calculator.getFinalPrice(1000)); // 1000

calculator.setStrategy(new FlatDiscountStrategy(200));
console.log(calculator.getFinalPrice(1000)); // 800

calculator.setStrategy(new PercentageDiscountStrategy(10));
console.log(calculator.getFinalPrice(1000)); // 900

Functional Approach (idiomatic JS)

In JavaScript you can also express strategies as plain functions — no classes needed.

const noDiscount      = (price) => price;
const flatDiscount = (amount) => (price) => price - amount;
const percentDiscount = (pct) => (price) => price - (price * pct) / 100;

function getPrice(price, strategy) {
return strategy(price);
}

getPrice(1000, noDiscount); // 1000
getPrice(1000, flatDiscount(200)); // 800
getPrice(1000, percentDiscount(10)); // 900

This is the shortest, most idiomatic JS version.

Use classes when strategies carry state or methods. Use functions when strategies are stateless transforms.


Real backend use cases in Node.js

1) Payment Gateway Selection

class RazorpayStrategy {
async pay(amount) {
console.log(`Processing ₹${amount} via Razorpay`);
}
}

class StripeStrategy {
async pay(amount) {
console.log(`Processing $${amount} via Stripe`);
}
}

class PaymentService {
constructor(strategy) {
this.strategy = strategy;
}

async checkout(amount) {
await this.strategy.pay(amount);
}
}

const service = new PaymentService(new RazorpayStrategy());
await service.checkout(500);

2) Delivery Partner Selection

class DunzoStrategy {
async createShipment(orderId) {
console.log(`Dunzo shipment created for ${orderId}`);
}
}

class ShadowfaxStrategy {
async createShipment(orderId) {
console.log(`Shadowfax shipment created for ${orderId}`);
}
}

class ShipmentService {
constructor(strategy) {
this.strategy = strategy;
}

async ship(orderId) {
await this.strategy.createShipment(orderId);
}
}

3) Carrier Integration with Strategy + Factory

A very common production pattern — the Factory picks the right strategy.

class CompanyStrategy {
async createShipment(order) {
return {
provider: 'Company',
payload: { orderId: order.id, customerName: order.customerName },
};
}
}

class PorterStrategy {
async createShipment(order) {
return {
provider: 'Porter',
payload: { shipment_id: order.id, name: order.customerName },
};
}
}

class ShadowfaxStrategy {
async createShipment(order) {
return {
provider: 'Shadowfax',
payload: { order_ref: order.id, recipient_name: order.customerName },
};
}
}

// Factory picks the strategy
const carrierStrategies = {
company: new CompanyStrategy(),
porter: new PorterStrategy(),
shadowfax: new ShadowfaxStrategy(),
};

function getCarrierStrategy(carrier) {
const strategy = carrierStrategies[carrier.toLowerCase()];
if (!strategy) throw new Error(`Unsupported carrier: ${carrier}`);
return strategy;
}

// Context
class CarrierService {
constructor(strategy) {
this.strategy = strategy;
}

async createShipment(order) {
return this.strategy.createShipment(order);
}
}

// Usage
const order = { id: 'ORD123', customerName: 'Prajwal' };

const strategy = getCarrierStrategy('porter');
const carrierService = new CarrierService(strategy);

const result = await carrierService.createShipment(order);
console.log(result);
// { provider: 'Porter', payload: { shipment_id: 'ORD123', name: 'Prajwal' } }

4) Notification Channel Strategy

class EmailStrategy {
async send(message) {
console.log('Email:', message);
}
}

class SmsStrategy {
async send(message) {
console.log('SMS:', message);
}
}

class WhatsAppStrategy {
async send(message) {
console.log('WhatsApp:', message);
}
}

class NotificationService {
constructor(strategy) {
this.strategy = strategy;
}

async notify(message) {
await this.strategy.send(message);
}
}

Benefits

1. Open/Closed Principle

Add new strategies without modifying existing code.

class FestivalDiscountStrategy {
calculate(price) {
return price * 0.7; // 30% off
}
}

No need to touch old logic.


2. Removes giant conditionals

No huge switch blocks in business logic.


3. Easy to test

// Jest
test('FlatDiscountStrategy subtracts flat amount', () => {
const strategy = new FlatDiscountStrategy(100);
expect(strategy.calculate(1000)).toBe(900);
});

Each strategy is a tiny, isolated unit.


4. Runtime flexibility

Swap behavior dynamically — from DB config, tenant settings, request params.


When to use Strategy Pattern

  • you have multiple interchangeable algorithms
  • behavior changes based on: provider, country, tenant, payment method, carrier, pricing logic
  • you want to add new variants without touching existing code

When NOT to use it

Do not use it if:

  • you only have 1 or 2 small conditions
  • abstraction makes code harder to read

Bad use:

if (isAdmin) {
// ...
} else {
// ...
}

Don't create 12 classes for that. That's design-pattern cosplay.


Strategy Pattern vs Factory Pattern

Strategy Pattern — focuses on behavior

"How should this task be performed?"

Factory Pattern — focuses on object creation

"Which object should I create?"

In real apps they are used together:

const strategy = getCarrierStrategy(carrier); // Factory
await strategy.createShipment(order); // Strategy

Interview definition (short answer)

"Strategy Pattern is a behavioral design pattern that allows selecting an algorithm or behavior at runtime by encapsulating each implementation into separate classes or functions behind a common interface."


Best mental model

If you see code like:

switch (provider) {
case 'x': ...
case 'y': ...
case 'z': ...
}

Ask yourself:

"Can this become a strategy?"

A lot of backend integration code should become one.


Formula:

Contract → Multiple Strategies → Context → Optional Factory